'use strict'

const assert = require('node:assert/strict')

const { expect } = require('chai')
const { afterEach, beforeEach, describe, it } = require('mocha')
const proxyquire = require('proxyquire')
const sinon = require('sinon')

describe('vulnerability-analyzer', () => {
  const VULNERABLE_VALUE = 'VULNERABLE_VALUE'
  const VULNERABILITY = 'VULNERABILITY'
  const VULNERABILITY_LOCATION_FROM_SOURCEMAP = {
    path: 'VULNERABILITY_LOCATION_FROM_SOURCEMAP', line: 42, class: 'process', method: 'processImmediate'
  }
  const ANALYZER_TYPE = 'TEST_ANALYZER'
  const SPAN_ID = '123456'

  let VulnerabilityAnalyzer
  let vulnerabilityReporter
  let overheadController
  let iastContextHandler

  beforeEach(() => {
    vulnerabilityReporter = {
      createVulnerability: sinon.stub().returns(VULNERABILITY),
      addVulnerability: sinon.stub(),
      replaceCallSiteFromSourceMap: sinon.stub().returns(VULNERABILITY_LOCATION_FROM_SOURCEMAP)
    }
    overheadController = {
      hasQuota: sinon.stub()
    }
    iastContextHandler = {
      getIastContext: sinon.stub()
    }

    VulnerabilityAnalyzer = proxyquire('../../../../src/appsec/iast/analyzers/vulnerability-analyzer', {
      '../vulnerability-reporter': vulnerabilityReporter,
      '../overhead-controller': overheadController,
      '../iast-context': iastContextHandler
    })
  })

  afterEach(() => {
    sinon.restore()
  })

  const shouldReportCasesData = [
    {
      iastContext: {},
      oceResult: true,
      isVulnerable: true,
      reported: true
    },
    {
      iastContext: {},
      oceResult: false,
      isVulnerable: false,
      reported: false
    },
    {
      iastContext: {},
      oceResult: false,
      isVulnerable: true,
      reported: false
    },
    {
      iastContext: {},
      oceResult: true,
      isVulnerable: false,
      reported: false
    },
    {
      iastContext: undefined,
      oceResult: true,
      isVulnerable: true,
      reported: true
    }
  ]
  shouldReportCasesData.forEach((shouldReportCaseData) => {
    it(`should ${shouldReportCaseData.reported ? 'report' : 'not report'} vulnerability
    when value is ${shouldReportCaseData.isVulnerable ? 'vulnerable' : 'not vulnerable'},
    OCE ${shouldReportCaseData.oceResult ? 'allows' : 'rejects'} the operation
    and iastContext is ${shouldReportCaseData.iastContext ? 'provided' : 'not provided'}`, () => {
      const vulnerabilityAnalyzer = new VulnerabilityAnalyzer(ANALYZER_TYPE)
      overheadController.hasQuota.returns(shouldReportCaseData.oceResult)
      iastContextHandler.getIastContext.returns(shouldReportCaseData.iastContext)
      sinon.stub(vulnerabilityAnalyzer, '_isVulnerable').returns(shouldReportCaseData.isVulnerable)
      sinon.stub(vulnerabilityAnalyzer, '_report')
      vulnerabilityAnalyzer.analyze(VULNERABLE_VALUE)
      if (shouldReportCaseData.reported) {
        sinon.assert.calledOnce(vulnerabilityAnalyzer._report)
      } else {
        expect(vulnerabilityAnalyzer._report).not.to.be.called
      }
    })
  })

  it('should return not vulnerable if not override', () => {
    const vulnerabilityAnalyzer = new VulnerabilityAnalyzer(ANALYZER_TYPE)
    const isVulnerable = vulnerabilityAnalyzer._isVulnerable()
    assert.strictEqual(isVulnerable, false)
  })

  it('should add vulnerability to reporter when reporting', () => {
    const vulnerabilityAnalyzer = new VulnerabilityAnalyzer(ANALYZER_TYPE)
    const context = {
      rootSpan: {
        context: sinon.mock().returns({
          toSpanId: sinon.mock().returns(SPAN_ID)
        })
      }
    }
    vulnerabilityAnalyzer._report(VULNERABLE_VALUE, context)
    sinon.assert.calledOnceWithExactly(vulnerabilityReporter.addVulnerability,
      context,
      {
        type: 'TEST_ANALYZER',
        evidence: {
          value: 'VULNERABLE_VALUE'
        },
        location: {
          spanId: '123456',
          stackId: '1',
          ...VULNERABILITY_LOCATION_FROM_SOURCEMAP
        },
        hash: 5975567724
      },
      sinon.match.array
    )
  })

  it('should catch thrown Errors inside subscription handlers', () => {
    const vulnerabilityAnalyzer = new VulnerabilityAnalyzer(ANALYZER_TYPE)
    vulnerabilityAnalyzer.addSub({ channelName: 'dd-trace:test:error:sub' }, () => {
      throw new Error('handler Error')
    })

    assert.strictEqual(vulnerabilityAnalyzer._subscriptions.length, 1)
    vulnerabilityAnalyzer._subscriptions[0].enable()

    const dc = require('dc-polyfill')
    assert.doesNotThrow(() => { dc.channel('dd-trace:test:error:sub').publish({}) })
  })

  describe('addSub', () => {
    let iastPluginAddSub

    beforeEach(() => {
      iastPluginAddSub = sinon.spy()

      class SinkIastPlugin {
        addSub (iastSub, handler) {
          iastPluginAddSub(iastSub, handler)
        }
      }

      VulnerabilityAnalyzer = proxyquire('../../../../src/appsec/iast/analyzers/vulnerability-analyzer', {
        '../vulnerability-reporter': vulnerabilityReporter,
        '../overhead-controller': overheadController,
        '../iast-context': iastContextHandler,
        '../iast-plugin': {
          SinkIastPlugin
        }
      })
    })

    it('should accept a string argument and convert it to iastSub object', () => {
      const vulnerabilityAnalyzer = new VulnerabilityAnalyzer(ANALYZER_TYPE)
      const handler = () => {}
      vulnerabilityAnalyzer.addSub('dd-trace:test:channelName', handler)

      sinon.assert.calledOnce(iastPluginAddSub)
      assert.deepStrictEqual(iastPluginAddSub.firstCall.args, [{
        channelName: 'dd-trace:test:channelName',
        tag: ANALYZER_TYPE
      }, handler])
    })

    it('should accept a iastSub object', () => {
      const vulnerabilityAnalyzer = new VulnerabilityAnalyzer(ANALYZER_TYPE)
      const handler = () => {}
      vulnerabilityAnalyzer.addSub({ channelName: 'dd-trace:test:channelName' }, handler)

      sinon.assert.calledOnce(iastPluginAddSub)
      assert.deepStrictEqual(iastPluginAddSub.firstCall.args, [{
        channelName: 'dd-trace:test:channelName',
        tag: ANALYZER_TYPE
      }, handler])
    })
  })

  describe('createVulnerability', () => {
    let vulnerabilityAnalyzer

    beforeEach(() => {
      vulnerabilityAnalyzer = new VulnerabilityAnalyzer(ANALYZER_TYPE)
    })

    it('invalid input does not break and creates null vulnerability', () => {
      assert.strictEqual(vulnerabilityAnalyzer._createVulnerability(), null)
      assert.strictEqual(vulnerabilityAnalyzer._createVulnerability(null), null)
      assert.strictEqual(vulnerabilityAnalyzer._createVulnerability(undefined), null)
      assert.strictEqual(vulnerabilityAnalyzer._createVulnerability(null, null), null)
      assert.strictEqual(vulnerabilityAnalyzer._createVulnerability(null, undefined), null)
      assert.strictEqual(vulnerabilityAnalyzer._createVulnerability(null, null, null), null)
      assert.strictEqual(vulnerabilityAnalyzer._createVulnerability(null, null, undefined), null)
      assert.strictEqual(vulnerabilityAnalyzer._createVulnerability(null, null, undefined, null), null)
      assert.strictEqual(vulnerabilityAnalyzer._createVulnerability(null, null, undefined, undefined), null)
      assert.strictEqual(vulnerabilityAnalyzer._createVulnerability('WEAK_HASHING', null, undefined, undefined), null)
    })

    it('creates the object with all properties', () => {
      const type = 'WEAK_HASHING'
      const evidence = { value: 'md5' }
      const location = { path: 'file-name.js', line: 15 }
      const spanId = 888
      const vulnerability = vulnerabilityAnalyzer._createVulnerability(type, evidence, spanId, location)
      assert.notStrictEqual(vulnerability, null)
      assert.strictEqual(vulnerability.type, type)
      assert.strictEqual(vulnerability.evidence, evidence)
      assert.strictEqual(vulnerability.location.path, location.path)
      assert.strictEqual(vulnerability.location.line, location.line)
      assert.strictEqual(vulnerability.location.spanId, spanId)
    })

    it('creates the object without location', () => {
      const type = 'WEAK_HASHING'
      const evidence = { value: 'md5' }
      const spanId = 888
      const vulnerability = vulnerabilityAnalyzer._createVulnerability(type, evidence, spanId)
      assert.notStrictEqual(vulnerability, null)
      assert.strictEqual(vulnerability.type, type)
      assert.strictEqual(vulnerability.evidence, evidence)
      assert.strictEqual(vulnerability.location.path, undefined)
      assert.strictEqual(vulnerability.location.line, undefined)
      assert.strictEqual(vulnerability.location.spanId, spanId)
    })

    it('creates the object without spanId', () => {
      const type = 'WEAK_HASHING'
      const evidence = { value: 'md5' }
      const location = { path: 'file-name.js', line: 15 }
      const vulnerability = vulnerabilityAnalyzer._createVulnerability(type, evidence, undefined, location)
      assert.notStrictEqual(vulnerability, null)
      assert.strictEqual(vulnerability.type, type)
      assert.strictEqual(vulnerability.evidence, evidence)
      assert.strictEqual(vulnerability.location.path, location.path)
      assert.strictEqual(vulnerability.location.line, location.line)
      assert.strictEqual(vulnerability.location.spanId, 0)
    })
  })

  describe('analyzeAll', () => {
    const value1 = 'test'
    const value2 = 'test2'

    it('should not throw with undefined values', () => {
      const vulnerabilityAnalyzer = new VulnerabilityAnalyzer(ANALYZER_TYPE)

      expect(() => vulnerabilityAnalyzer.analyzeAll(undefined, value1, undefined)).to.not.throw
    })

    it('should analyze batch of values', () => {
      const vulnerabilityAnalyzer = new VulnerabilityAnalyzer(ANALYZER_TYPE)
      const context = {
        rootSpan: {
          context: sinon.mock().returns({
            toSpanId: sinon.mock().returns(SPAN_ID)
          })
        }
      }
      iastContextHandler.getIastContext.returns(context)
      sinon.stub(vulnerabilityAnalyzer, '_isVulnerable').returns((value) => value === 'test')
      sinon.stub(vulnerabilityAnalyzer, '_checkOCE').returns(true)
      sinon.stub(vulnerabilityAnalyzer, '_createVulnerability')

      vulnerabilityAnalyzer.analyzeAll(value1, value2)

      sinon.assert.calledOnceWithMatch(vulnerabilityAnalyzer._createVulnerability,
        ANALYZER_TYPE,
        { value: 'test' },
        SPAN_ID,
        VULNERABILITY_LOCATION_FROM_SOURCEMAP
      )
    })
  })
})
